/*
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.  
 */

package erwiki.plugins;

import java.security.Principal;
import java.util.Map;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.PatternCompiler;
import org.apache.oro.text.regex.PatternMatcher;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;
import org.elwiki.IWikiConstants;

import erwiki.api.WikiPartReference;
import erwiki.api.auth.AuthorizationManager;
import erwiki.api.core.WikiContext;
import erwiki.api.exceptions.PluginException;
import erwiki.api.pages.PageManager;
import erwiki.api.plugin.PluginManager;
import erwiki.api.plugin.WikiPlugin;
import erwiki.api.providers.WikiProvider;
import erwiki.api.render.RenderingManager;
import erwiki.api.variables.VariableManager;
import erwiki.util.HttpUtil;

/**
 * Плагин ErWiki. IfPlugin позволяет условно выполнять части WikiPage и предназначен для гибкой
 * настройки страницы, в зависимости от определенных условий. Не используйте его в качестве
 * механизма безопасности для обеспечения управляемого сокрытия контента от пользователей (для этого
 * следует использовать ACL страницы).</br>
 * Для запуска этого плагина можно использовать сокращение "If".
 *
 * <p>
 * Параметры плагина:
 * <ul>
 * <li><b>group</b> - список имен групп, разделенный "|".
 * <li><b>user</b> - список имен пользователей, разделенный "|".
 * <li><b>ip</b> - список IP-адресов, разделенный "|".
 * <li><b>var</b> - wiki-переменная.
 * <li><b>page</b> - имя страницы.
 * <li><b>contains</b> - шаблон регулярного выражения Perl5.
 * <li><b>is</b> - шаблон регулярного выражения Perl5.
 * <li><b>exists</b> - "true" или "false".
 * </ul>
 * 
 * <p>
 * Если какой-либо из параметров совпадает с текущим контекстом wiki, то выполняется тело плагина.
 * Условие можно инвертировать, добавив префикс "!". Например, чтобы поприветствовать всех
 * администраторов, поместите в LeftMenu следующее:
 * 
 * <pre>
 *  [{If group='Admin'
 *  Hello, Admin, and your mighty powers!}]
 * </pre>
 * 
 * <p>
 * Чтобы отправить сообщение всем, кроме Джека, используйте следующее:
 * 
 * <pre>
 *  [{If user='!Jack'
 *  %%warning
 *  В одиннадцать - вечеринка-сюрприз, по случаю дня рождения Джека!
 *  %%}]
 * </pre>
 * 
 * <p>
 * Обратите внимание, что вы не можете использовать "!Jack|!Jill", поскольку для Jack соответствует
 * !Jill; а для Jill, !Jack. Это не регулярные выражения (хотя они могут стать таковыми в будущем).
 * <p>
 * Чтобы проверить содержимое страницы, используйте:
 * 
 * <pre>
 *  [{If page='TestPage' contains='xyzzy'
 *  Page contains the text "xyzzy"}]
 * </pre>
 *
 * <p>
 * Разница между "contains" и "is" заключается в том, что "is" всегда является точным совпадением,
 * тогда как "contains" просто проверяет, доступен ли шаблон.
 *
 * <p>
 * Чтобы проверить существование страницы, используйте
 * 
 * <pre>
 *  [{If page='TestPage' exists='true'
 *  Page "TestPage" exists.}]
 * </pre>
 * 
 * <p>
 * С помощью того же механизма можно проверить существование переменной – просто используйте "var"
 * вместо "page".
 */
public class IfPlugin implements WikiPlugin {

	/** Имя параметра для настройки проверки <i>группы</i>. Значение <tt>{@value}</tt>. */
	public static final String PARAM_GROUP = "group";

	/** Имя параметра для настройки проверки <i>id пользователя</i>. Значение <tt>{@value}</tt>. */
	public static final String PARAM_USER = "user";

	/** Имя параметра для настройки проверки <i>ip адреса</i>. Значение <tt>{@value}</tt>. */
	public static final String PARAM_IP = "ip";

	/** Имя параметра для настройки проверки <i>имени страницы</i>. Значение <tt>{@value}</tt>. */
	public static final String PARAM_PAGE = "page";

	/** Имя параметра для настройки проверки <i>содержимого страницы</i>. Значение <tt>{@value}</tt>. */
	public static final String PARAM_CONTAINS = "contains";

	/** Имя параметра для настройки проверки <i>имени переменной</i>. Значение <tt>{@value}</tt>. */
	public static final String PARAM_VAR = "var";

	/** Имя параметра для настройки проверки <i>точного содержимого</i>. Значение <tt>{@value}</tt>. */
	public static final String PARAM_IS = "is";

	/**
	 * Имя параметра для проверки существования <i>страницы/переменной</i>. Значение <tt>{@value}</tt>.
	 */
	public static final String PARAM_EXISTS = "exists";

	@WikiPartReference
	private AuthorizationManager authorizationManager;

	@WikiPartReference
	private PageManager pageManager;

	@WikiPartReference
	private RenderingManager renderingManager;

	@WikiPartReference
	private VariableManager variableManager;

	/** {@inheritDoc} */
	@Override
	public String execute(WikiContext context, Map<String, String> params) throws PluginException {
		return ifInclude(context, params)
				? this.renderingManager.textToHTML(params.get(PluginManager.PARAM_BODY))
				: "";
	}

	/**
	 * Возвращает boolean результат, на основе обработки WikiContext и карты параметров, согласно
	 * правилам, изложенных в документации IfPlugin.
	 *
	 * @param context Текущий WikiContext.
	 * @param params  Параметр Map, содержащий пары ключ-значение.
	 * @throws PluginException Если что-то пойдет не так.
	 * @return <code>True</code>, если условие выполняется.
	 */
	protected boolean ifInclude(WikiContext context, Map<String, String> params) throws PluginException {
		String group = params.get(PARAM_GROUP);
		String user = params.get(PARAM_USER);
		String ip = params.get(PARAM_IP);
		String pageName = params.get(PARAM_PAGE);
		String contains = params.get(PARAM_CONTAINS);
		String var = params.get(PARAM_VAR);
		String is = params.get(PARAM_IS);
		String exists = params.get(PARAM_EXISTS);

		boolean include = checkGroup(context, group);
		include |= checkUser(context, user);
		include |= checkIP(context, ip);

		if (pageName != null) {
			String content = this.pageManager.getPureText(pageName, IWikiConstants.LATEST_VERSION).trim();
			include |= checkContains(content, contains);
			include |= checkIs(content, is);
			include |= checkExists(context, pageName, exists);
		}

		if (var != null) {
			String content = this.variableManager.getVariable(var);
			include |= checkContains(content, contains);
			include |= checkIs(content, is);
			include |= checkVarExists(content, exists);
		}

		return include;
	}

	private boolean checkExists(WikiContext context, String page, String exists) {
		if (exists == null) {
			return false;
		}

		var value = BooleanUtils.toBooleanObject(exists);
		boolean isExists = (value != null) ? value : false;
		return !this.pageManager.pageExistsByName(page) ^ isExists;
	}

	private boolean checkVarExists(String varContent, String exists) {
		if (exists == null) {
			return false;
		}

		var value = BooleanUtils.toBooleanObject(exists);
		boolean isExists = (value != null) ? value : false;
		return varContent == null ^ isExists;
	}

	private boolean checkGroup(WikiContext context, String group) {
		if (group == null) {
			return false;
		}
		String[] groupList = StringUtils.split(group, '|');
		boolean include = false;

		for (String grp : groupList) {
			String gname = grp;
			boolean invert = false;
			if (grp.startsWith("!")) {
				if (grp.length() > 1) {
					gname = grp.substring(1);
				}
				invert = true;
			}

			Principal g = this.authorizationManager.resolvePrincipal(gname);

			include |= this.authorizationManager.isUserInRole(context.getWikiSession(), g) ^ invert;
		}

		return include;
	}

	private boolean checkUser(WikiContext context, String user) {
		if (user == null || context.getCurrentUser() == null) {
			return false;
		}

		String[] list = StringUtils.split(user, '|');
		boolean include = false;

		for (String usr : list) {
			String userToCheck = usr;
			boolean invert = false;
			if (usr.startsWith("!")) {
				invert = true;
				// strip !
				if (user.length() > 1) {
					userToCheck = usr.substring(1);
				}
			}

			include |= userToCheck.equals(context.getCurrentUser().getName()) ^ invert;
		}

		return include;
	}

	// TODO: Add subnetwork matching, e.g. 10.0.0.0/8
	private boolean checkIP(WikiContext context, String ipaddr) {
		if (ipaddr == null || context.getHttpRequest() == null) {
			return false;
		}

		String[] list = StringUtils.split(ipaddr, '|');
		boolean include = false;

		for (String ip : list) {
			String ipaddrToCheck = ip;
			boolean invert = false;
			if (ip.startsWith("!")) {
				invert = true;
				// strip !
				if (ip.length() > 1) {
					ipaddrToCheck = ip.substring(1);
				}
			}

			include |= ipaddrToCheck.equals(HttpUtil.getRemoteAddress(context.getHttpRequest())) ^ invert;
		}

		return include;
	}

	private boolean doMatch(String content, String pattern) throws PluginException {
		PatternCompiler compiler = new Perl5Compiler();
		PatternMatcher matcher = new Perl5Matcher();

		try {
			Pattern matchp = compiler.compile(pattern, Perl5Compiler.SINGLELINE_MASK);
			return matcher.matches(content, matchp);
		} catch (MalformedPatternException e) {
			throw new PluginException("Faulty pattern " + pattern);
		}
	}

	private boolean checkContains(String pagecontent, String matchPattern) throws PluginException {
		if (pagecontent == null || matchPattern == null) {
			return false;
		}

		return doMatch(pagecontent, ".*" + matchPattern + ".*");
	}

	private boolean checkIs(String content, String matchPattern) throws PluginException {
		if (content == null || matchPattern == null) {
			return false;
		}

		return doMatch(content, "^" + matchPattern + "$");
	}

}
